Skip to content

Conversation

@minwoo1999
Copy link
Member

@minwoo1999 minwoo1999 commented Aug 16, 2025

🔗 관련 이슈

📘 작업 유형

  • 🐞 Bugfix (버그 수정)

📙 작업 내역

  • JpaReadingRecordQuerydslRepositoryImpl의 정렬 조건 수정
    • PAGE_NUMBER_ASC / DESC 정렬 시 동일 pageNumber를 가진 데이터가 섞이지 않도록 updatedAt.desc() 보조 정렬 조건 추가
    • 기타 정렬(CREATED_DATE_ASC / DESC, 기본 정렬)은 기존 로직 유지

🧪 테스트 내역

  • PAGE_NUMBER_ASC 정렬 시 동일 페이지 번호 내 최신 수정 순서대로 정렬됨 확인
  • PAGE_NUMBER_DESC 정렬 시 동일 페이지 번호 내 최신 수정 순서대로 정렬됨 확인
  • 기존 CREATED_DATE 기준 정렬 영향 없음

💬 리뷰 포인트

  • 보조 정렬 기준을 updatedAt으로 두는 게 적절한지, 혹은 createdAt이나 다른 기준이 더 나을지 확인 부탁드립니다.

Summary by CodeRabbit

  • 신기능
    • 정렬 고도화: 페이지 번호 오름/내림차순 선택 시, 동일 페이지 번호 내에서는 최신 업데이트 기준으로 2차 정렬되어 결과가 더 안정적이고 예측 가능해졌습니다.
  • 개선
    • 생성일 기준 정렬은 기존 동작을 유지하면서 내부적으로 일관된 처리 방식을 적용해 정렬 신뢰도를 높였습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

페이지 번호 정렬 시 안정성을 확보하기 위해 단일 OrderSpecifier 반환을 배열(Array<OrderSpecifier<*>>)로 변경하고, 페이지 번호 정렬일 때 updatedAt desc를 보조 정렬로 추가했다. 메서드명을 createOrderSpecifier → createOrderSpecifiers로 변경했으며, 페이징 및 총계 계산 로직은 변경 없다.

Changes

Cohort / File(s) Summary
Querydsl 정렬 로직 보강
infra/src/main/kotlin/.../JpaReadingRecordQuerydslRepositoryImpl.kt
단일 정렬 → 다중 정렬로 변경. pageNumber asc/desc 시 updatedAt desc 보조 정렬 추가. CREATED_DATE_* 케이스는 배열 래핑. 메서드명 및 호출부(orderBy)에 배열 전개 적용.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Repository
  participant QueryDSL
  participant DB

  Caller->>Repository: findReadingRecords(sort, page)
  Repository->>QueryDSL: build query with orderBy(pageNumber, updatedAt desc) or createdAt
  QueryDSL->>DB: SELECT ... ORDER BY ...
  DB-->>QueryDSL: rows
  QueryDSL-->>Repository: results
  Repository-->>Caller: paged results
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
페이지번호 정렬 시 페이징 중복 ID 발생 방지 위한 정렬 안정화 (#90)

Assessment against linked issues: Out-of-scope changes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-255-fix/#90

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a27f214 and 244ca07.

📒 Files selected for processing (1)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (2 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt

[warning] 34-34: In most cases using a spread operator causes a full copy of the array to be created before calling a method. This may result in a performance penalty.

(detekt.performance.SpreadOperator)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-validation
🔇 Additional comments (1)
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1)

34-34: 정렬 대상 컬럼은 모두 non-null 제약이 있습니다

  • infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt
    · @Column(name = "page_number", nullable = false)
  • infra/src/main/kotlin/org/yapp/infra/common/BaseTimeEntity.kt
    · @Column(name = "created_at", nullable = false)
    · @Column(name = "updated_at", nullable = false)

따라서 DB에서 해당 칼럼이 NULL이 될 가능성이 없으므로 별도의 nullsLast()/nullsFirst() 지정은 필요하지 않습니다.

.selectFrom(readingRecord)
.where(whereCondition)
.orderBy(createOrderSpecifier(sort))
.orderBy(*createOrderSpecifiers(sort))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

스프레드 연산자 사용(detekt 경고) — 영향 미미하나 억제 또는 구조 변경을 고려하세요

.orderBy(*createOrderSpecifiers(sort))는 detekt의 SpreadOperator 경고 대상입니다. 현재 배열 길이가 1~2 수준이라 성능 영향은 사실상 미미합니다. 유지보수를 위해 그대로 두되 경고 억제 또는 호출부 구조 변경 중 하나를 권장합니다.

대안 1: 함수 레벨에서 경고 억제

@Suppress("SpreadOperator")
override fun findReadingRecordsByDynamicCondition(
    userBookId: UUID,
    sort: ReadingRecordSortType?,
    pageable: Pageable
): Page<ReadingRecordEntity> {
    // ...
}

대안 2: 스프레드 없이 체이닝

  • createOrderSpecifiers를 사용하지 않고 when 분기 내에서 .orderBy(a, b) 형태로 직접 지정
  • 변경폭이 커지므로 본 PR 범위를 넘는다면 대안 1이 현실적입니다.
🧰 Tools
🪛 detekt (1.23.8)

[warning] 34-34: In most cases using a spread operator causes a full copy of the array to be created before calling a method. This may result in a performance penalty.

(detekt.performance.SpreadOperator)

🤖 Prompt for AI Agents
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
around line 34: Detekt flags the spread operator in
`.orderBy(*createOrderSpecifiers(sort))`; to address this either suppress the
warning on the function with @Suppress("SpreadOperator") or remove the spread by
inlining orderBy arguments (use explicit chaining or branch on sort and call
.orderBy(a, b) directly); pick suppression if you want minimal changes, or
refactor the ordering logic to avoid creating and spreading an array if you
prefer to eliminate the warning fully.

Comment on lines +48 to +59
private fun createOrderSpecifiers(sort: ReadingRecordSortType?): Array<OrderSpecifier<*>> {
return when (sort) {
ReadingRecordSortType.PAGE_NUMBER_ASC -> readingRecord.pageNumber.asc()
ReadingRecordSortType.PAGE_NUMBER_DESC -> readingRecord.pageNumber.desc()
ReadingRecordSortType.CREATED_DATE_ASC -> readingRecord.createdAt.asc()
ReadingRecordSortType.CREATED_DATE_DESC -> readingRecord.createdAt.desc()
null -> readingRecord.createdAt.desc()
ReadingRecordSortType.PAGE_NUMBER_ASC -> arrayOf(
readingRecord.pageNumber.asc(),
readingRecord.updatedAt.desc()
)

ReadingRecordSortType.PAGE_NUMBER_DESC -> arrayOf(
readingRecord.pageNumber.desc(),
readingRecord.updatedAt.desc()
)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

🛠️ Refactor suggestion

타이브레이커 부족 — id로 최종 정렬 기준을 추가해 페이징 안정성을 보장하세요

updatedAt.desc()를 보조 정렬로 추가한 것은 합리적입니다. 다만 동일 pageNumber와 동일 updatedAt이 동시에 존재할 경우(타임스탬프 해상도/동시 업데이트 등) 여전히 정렬이 비결정적이어서 오프셋 페이징에서 중복/누락이 재현될 수 있습니다. 최종 타이브레이커로 유일 키(일반적으로 id)를 추가해 결정적 정렬을 보장하세요.

권장 변경(diff):

             ReadingRecordSortType.PAGE_NUMBER_ASC -> arrayOf(
                 readingRecord.pageNumber.asc(),
-                readingRecord.updatedAt.desc()
+                readingRecord.updatedAt.desc(),
+                readingRecord.id.desc()
             )

             ReadingRecordSortType.PAGE_NUMBER_DESC -> arrayOf(
                 readingRecord.pageNumber.desc(),
-                readingRecord.updatedAt.desc()
+                readingRecord.updatedAt.desc(),
+                readingRecord.id.desc()
             )
  • updatedAt vs createdAt에 대한 질문에 답변: “페이지 번호 내에서 최근에 수정된 기록을 우선”이라는 제품 의도가 있다면 updatedAt.desc()createdAt보다 더 적절합니다. 단, “등록 순서” 의미가 더 중요하다면 createdAt을 택할 수 있습니다. 어떤 의미가 UX/도메인에 맞는지 팀 합의가 필요합니다.

성능(인덱스) 관점:

  • 현재 where: (userBookId, deletedAt IS NULL) + order: (pageNumber [asc/desc], updatedAt desc, id desc) 조합을 자주 사용한다면 해당 컬럼 순서로의 복합 인덱스 추가를 고려하세요.
    • 예: (user_book_id, deleted_at, page_number, updated_at, id)
  • createdAt 정렬 경로도 빈번하다면 (user_book_id, deleted_at, created_at, id) 인덱스가 유효합니다.

오프셋 페이징의 본질적 한계:

  • updatedAt이 자주 변하는 도메인이라면 페이지를 넘기는 사이에 레코드가 이동하여 중복/누락이 발생할 수 있습니다. 장기적으로는 keyset 페이징(커서 기반) 전환을 검토하세요.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun createOrderSpecifiers(sort: ReadingRecordSortType?): Array<OrderSpecifier<*>> {
return when (sort) {
ReadingRecordSortType.PAGE_NUMBER_ASC -> readingRecord.pageNumber.asc()
ReadingRecordSortType.PAGE_NUMBER_DESC -> readingRecord.pageNumber.desc()
ReadingRecordSortType.CREATED_DATE_ASC -> readingRecord.createdAt.asc()
ReadingRecordSortType.CREATED_DATE_DESC -> readingRecord.createdAt.desc()
null -> readingRecord.createdAt.desc()
ReadingRecordSortType.PAGE_NUMBER_ASC -> arrayOf(
readingRecord.pageNumber.asc(),
readingRecord.updatedAt.desc()
)
ReadingRecordSortType.PAGE_NUMBER_DESC -> arrayOf(
readingRecord.pageNumber.desc(),
readingRecord.updatedAt.desc()
)
private fun createOrderSpecifiers(sort: ReadingRecordSortType?): Array<OrderSpecifier<*>> {
return when (sort) {
ReadingRecordSortType.PAGE_NUMBER_ASC -> arrayOf(
readingRecord.pageNumber.asc(),
readingRecord.updatedAt.desc(),
readingRecord.id.desc()
)
ReadingRecordSortType.PAGE_NUMBER_DESC -> arrayOf(
readingRecord.pageNumber.desc(),
readingRecord.updatedAt.desc(),
readingRecord.id.desc()
)
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
around lines 48–59, the order specifiers lack a final deterministic tiebreaker
which can cause nondeterministic paging when pageNumber and updatedAt are equal;
add the primary key (id) as the last OrderSpecifier (descending or ascending
consistent with desired newest-first/oldest-first tie-breaking) to both
PAGE_NUMBER_ASC and PAGE_NUMBER_DESC branches so sorting is fully deterministic;
optionally confirm whether updatedAt or createdAt is the intended second-level
sort and, if this sort pattern is frequent, consider adding a composite DB index
(e.g., user_book_id, deleted_at, page_number, updated_at, id) to support the new
ordering.

Comment on lines +50 to +53
ReadingRecordSortType.PAGE_NUMBER_ASC -> arrayOf(
readingRecord.pageNumber.asc(),
readingRecord.updatedAt.desc()
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

updatedAt을 보조 정렬로 사용한 선택은 타당합니다

동일 pageNumber 내에서 “최근 수정 기준”을 기대한다면 updatedAt.desc()는 도메인 의도에 부합합니다. 위 코멘트대로 최종 타이브레이커(id)만 추가해 주시면 더 이상 중복 위험이 없습니다.

🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
around lines 50 to 53, the sort array uses pageNumber and updatedAt but lacks a
final tiebreaker; append the entity id comparator as the last sort key (e.g.,
readingRecord.id.asc() or .desc() depending on desired stable order) so results
are deterministic and duplicate-key risks are eliminated.

Comment on lines +60 to 63
ReadingRecordSortType.CREATED_DATE_ASC -> arrayOf(readingRecord.createdAt.asc())
ReadingRecordSortType.CREATED_DATE_DESC -> arrayOf(readingRecord.createdAt.desc())
null -> arrayOf(readingRecord.createdAt.desc())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

createdAt 정렬에도 유일 타이브레이커(id) 추가를 고려

본 PR 범위를 넘는 제안이지만, CREATED_DATE_ASC/DESC에서도 동일 타임스탬프가 가능하므로 최종적으로 id를 추가하면 전역적으로 페이징 안정성이 향상됩니다.

권장 변경(diff):

-            ReadingRecordSortType.CREATED_DATE_ASC -> arrayOf(readingRecord.createdAt.asc())
-            ReadingRecordSortType.CREATED_DATE_DESC -> arrayOf(readingRecord.createdAt.desc())
-            null -> arrayOf(readingRecord.createdAt.desc())
+            ReadingRecordSortType.CREATED_DATE_ASC -> arrayOf(
+                readingRecord.createdAt.asc(),
+                readingRecord.id.desc()
+            )
+            ReadingRecordSortType.CREATED_DATE_DESC -> arrayOf(
+                readingRecord.createdAt.desc(),
+                readingRecord.id.desc()
+            )
+            null -> arrayOf(
+                readingRecord.createdAt.desc(),
+                readingRecord.id.desc()
+            )

단위 테스트 보강 제안:

  • 동일 createdAt/updatedAt를 가진 샘플 데이터로 오프셋 페이징 시 중복/누락이 없는지 검증하는 테스트를 추가하면 회귀 방지에 도움이 됩니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ReadingRecordSortType.CREATED_DATE_ASC -> arrayOf(readingRecord.createdAt.asc())
ReadingRecordSortType.CREATED_DATE_DESC -> arrayOf(readingRecord.createdAt.desc())
null -> arrayOf(readingRecord.createdAt.desc())
}
ReadingRecordSortType.CREATED_DATE_ASC -> arrayOf(
readingRecord.createdAt.asc(),
readingRecord.id.desc()
)
ReadingRecordSortType.CREATED_DATE_DESC -> arrayOf(
readingRecord.createdAt.desc(),
readingRecord.id.desc()
)
null -> arrayOf(
readingRecord.createdAt.desc(),
readingRecord.id.desc()
)
}
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
around lines 60 to 63, the sorting for CREATED_DATE_ASC/DESC currently only uses
createdAt which can produce non-deterministic ordering when timestamps are
identical; update the sort arrays to add id as a deterministic tie-breaker (for
CREATED_DATE_ASC use createdAt.asc(), id.asc(); for CREATED_DATE_DESC use
createdAt.desc(), id.desc(); keep the null default as createdAt.desc(),
id.desc()). Also add unit tests that create multiple records with identical
createdAt/updatedAt values and assert offset-based paging returns stable,
non-duplicated/omitted results.

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~

@minwoo1999 minwoo1999 merged commit f0c7431 into develop Aug 16, 2025
6 checks passed
@move-hoon move-hoon changed the title 독서 기록 pagenumber로 order by 시 페이징처리 동일한 id가 내려오는 상황 fix: 독서 기록 pagenumber로 order by 시 페이징처리 동일한 id가 내려오는 상황 Aug 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-255/fix] 독서 기록 pagenumber로 order by 시 페이징처리 동일한 id가 내려오는 상황

3 participants